/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.transaction.support;

import java.io.Serializable;

import org.springframework.core.Constants;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;

/**
 * Default implementation of the {@link TransactionDefinition} interface,
 * offering bean-style configuration and sensible default values
 * (PROPAGATION_REQUIRED, ISOLATION_DEFAULT, TIMEOUT_DEFAULT, readOnly=false).
 *
 * <p>Base class for both {@link TransactionTemplate} and
 * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute}.
 *
 * @author Juergen Hoeller
 * @since 08.05.2003
 */
@SuppressWarnings("serial")
public class DefaultTransactionDefinition implements TransactionDefinition, Serializable {

    /**
     * Prefix for the propagation constants defined in TransactionDefinition
     */
    public static final String PREFIX_PROPAGATION = "PROPAGATION_";

    /**
     * Prefix for the isolation constants defined in TransactionDefinition
     */
    public static final String PREFIX_ISOLATION = "ISOLATION_";

    /**
     * Prefix for transaction timeout values in description strings
     */
    public static final String PREFIX_TIMEOUT = "timeout_";

    /**
     * Marker for read-only transactions in description strings
     */
    public static final String READ_ONLY_MARKER = "readOnly";


    /**
     * Constants instance for TransactionDefinition
     */
    static final Constants constants = new Constants(TransactionDefinition.class);

    private int propagationBehavior = PROPAGATION_REQUIRED;

    private int isolationLevel = ISOLATION_DEFAULT;

    private int timeout = TIMEOUT_DEFAULT;

    private boolean readOnly = false;

    @Nullable
    private String name;


    /**
     * Create a new DefaultTransactionDefinition, with default settings.
     * Can be modified through bean property setters.
     *
     * @see #setPropagationBehavior
     * @see #setIsolationLevel
     * @see #setTimeout
     * @see #setReadOnly
     * @see #setName
     */
    public DefaultTransactionDefinition() {
    }

    /**
     * Copy constructor. Definition can be modified through bean property setters.
     *
     * @see #setPropagationBehavior
     * @see #setIsolationLevel
     * @see #setTimeout
     * @see #setReadOnly
     * @see #setName
     */
    public DefaultTransactionDefinition(TransactionDefinition other) {
        this.propagationBehavior = other.getPropagationBehavior();
        this.isolationLevel = other.getIsolationLevel();
        this.timeout = other.getTimeout();
        this.readOnly = other.isReadOnly();
        this.name = other.getName();
    }

    /**
     * Create a new DefaultTransactionDefinition with the given
     * propagation behavior. Can be modified through bean property setters.
     *
     * @param propagationBehavior one of the propagation constants in the
     * TransactionDefinition interface
     * @see #setIsolationLevel
     * @see #setTimeout
     * @see #setReadOnly
     */
    public DefaultTransactionDefinition(int propagationBehavior) {
        this.propagationBehavior = propagationBehavior;
    }


    /**
     * Set the propagation behavior by the name of the corresponding constant in
     * TransactionDefinition, e.g. "PROPAGATION_REQUIRED".
     *
     * @param constantName name of the constant
     * @throws IllegalArgumentException if the supplied value is not resolvable
     * to one of the {@code PROPAGATION_} constants or is {@code null}
     * @see #setPropagationBehavior
     * @see #PROPAGATION_REQUIRED
     */
    public final void setPropagationBehaviorName(String constantName) throws IllegalArgumentException {
        if (!constantName.startsWith(PREFIX_PROPAGATION)) {
            throw new IllegalArgumentException("Only propagation constants allowed");
        }
        setPropagationBehavior(constants.asNumber(constantName).intValue());
    }

    @Override
    public final int getPropagationBehavior() {
        return this.propagationBehavior;
    }

    /**
     * Set the propagation behavior. Must be one of the propagation constants
     * in the TransactionDefinition interface. Default is PROPAGATION_REQUIRED.
     * <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
     * {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
     * transactions. Consider switching the "validateExistingTransactions" flag to
     * "true" on your transaction manager if you'd like isolation level declarations
     * to get rejected when participating in an existing transaction with a different
     * isolation level.
     * <p>Note that a transaction manager that does not support custom isolation levels
     * will throw an exception when given any other level than {@link #ISOLATION_DEFAULT}.
     *
     * @throws IllegalArgumentException if the supplied value is not one of the
     * {@code PROPAGATION_} constants
     * @see #PROPAGATION_REQUIRED
     */
    public final void setPropagationBehavior(int propagationBehavior) {
        if (!constants.getValues(PREFIX_PROPAGATION).contains(propagationBehavior)) {
            throw new IllegalArgumentException("Only values of propagation constants allowed");
        }
        this.propagationBehavior = propagationBehavior;
    }

    /**
     * Set the isolation level by the name of the corresponding constant in
     * TransactionDefinition, e.g. "ISOLATION_DEFAULT".
     *
     * @param constantName name of the constant
     * @throws IllegalArgumentException if the supplied value is not resolvable
     * to one of the {@code ISOLATION_} constants or is {@code null}
     * @see #setIsolationLevel
     * @see #ISOLATION_DEFAULT
     */
    public final void setIsolationLevelName(String constantName) throws IllegalArgumentException {
        if (!constantName.startsWith(PREFIX_ISOLATION)) {
            throw new IllegalArgumentException("Only isolation constants allowed");
        }
        setIsolationLevel(constants.asNumber(constantName).intValue());
    }

    @Override
    public final int getIsolationLevel() {
        return this.isolationLevel;
    }

    /**
     * Set the isolation level. Must be one of the isolation constants
     * in the TransactionDefinition interface. Default is ISOLATION_DEFAULT.
     * <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
     * {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
     * transactions. Consider switching the "validateExistingTransactions" flag to
     * "true" on your transaction manager if you'd like isolation level declarations
     * to get rejected when participating in an existing transaction with a different
     * isolation level.
     * <p>Note that a transaction manager that does not support custom isolation levels
     * will throw an exception when given any other level than {@link #ISOLATION_DEFAULT}.
     *
     * @throws IllegalArgumentException if the supplied value is not one of the
     * {@code ISOLATION_} constants
     * @see #ISOLATION_DEFAULT
     */
    public final void setIsolationLevel(int isolationLevel) {
        if (!constants.getValues(PREFIX_ISOLATION).contains(isolationLevel)) {
            throw new IllegalArgumentException("Only values of isolation constants allowed");
        }
        this.isolationLevel = isolationLevel;
    }

    @Override
    public final int getTimeout() {
        return this.timeout;
    }

    /**
     * Set the timeout to apply, as number of seconds.
     * Default is TIMEOUT_DEFAULT (-1).
     * <p>Exclusively designed for use with {@link #PROPAGATION_REQUIRED} or
     * {@link #PROPAGATION_REQUIRES_NEW} since it only applies to newly started
     * transactions.
     * <p>Note that a transaction manager that does not support timeouts will throw
     * an exception when given any other timeout than {@link #TIMEOUT_DEFAULT}.
     *
     * @see #TIMEOUT_DEFAULT
     */
    public final void setTimeout(int timeout) {
        if (timeout < TIMEOUT_DEFAULT) {
            throw new IllegalArgumentException("Timeout must be a positive integer or TIMEOUT_DEFAULT");
        }
        this.timeout = timeout;
    }

    @Override
    public final boolean isReadOnly() {
        return this.readOnly;
    }

    /**
     * Set whether to optimize as read-only transaction.
     * Default is "false".
     * <p>The read-only flag applies to any transaction context, whether backed
     * by an actual resource transaction ({@link #PROPAGATION_REQUIRED}/
     * {@link #PROPAGATION_REQUIRES_NEW}) or operating non-transactionally at
     * the resource level ({@link #PROPAGATION_SUPPORTS}). In the latter case,
     * the flag will only apply to managed resources within the application,
     * such as a Hibernate {@code Session}.
     * <p>This just serves as a hint for the actual transaction subsystem;
     * it will <i>not necessarily</i> cause failure of write access attempts.
     * A transaction manager which cannot interpret the read-only hint will
     * <i>not</i> throw an exception when asked for a read-only transaction.
     */
    public final void setReadOnly(boolean readOnly) {
        this.readOnly = readOnly;
    }

    @Override
    @Nullable
    public final String getName() {
        return this.name;
    }

    /**
     * Set the name of this transaction. Default is none.
     * <p>This will be used as transaction name to be shown in a
     * transaction monitor, if applicable (for example, WebLogic's).
     */
    public final void setName(String name) {
        this.name = name;
    }

    /**
     * This implementation compares the {@code toString()} results.
     *
     * @see #toString()
     */
    @Override
    public boolean equals(Object other) {
        return (other instanceof TransactionDefinition && toString().equals(other.toString()));
    }

    /**
     * This implementation returns {@code toString()}'s hash code.
     *
     * @see #toString()
     */
    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    /**
     * Return an identifying description for this transaction definition.
     * <p>The format matches the one used by
     * {@link org.springframework.transaction.interceptor.TransactionAttributeEditor},
     * to be able to feed {@code toString} results into bean properties of type
     * {@link org.springframework.transaction.interceptor.TransactionAttribute}.
     * <p>Has to be overridden in subclasses for correct {@code equals}
     * and {@code hashCode} behavior. Alternatively, {@link #equals}
     * and {@link #hashCode} can be overridden themselves.
     *
     * @see #getDefinitionDescription()
     * @see org.springframework.transaction.interceptor.TransactionAttributeEditor
     */
    @Override
    public String toString() {
        return getDefinitionDescription().toString();
    }

    /**
     * Return an identifying description for this transaction definition.
     * <p>Available to subclasses, for inclusion in their {@code toString()} result.
     */
    protected final StringBuilder getDefinitionDescription() {
        StringBuilder result = new StringBuilder();
        result.append(constants.toCode(this.propagationBehavior, PREFIX_PROPAGATION));
        result.append(',');
        result.append(constants.toCode(this.isolationLevel, PREFIX_ISOLATION));
        if (this.timeout != TIMEOUT_DEFAULT) {
            result.append(',');
            result.append(PREFIX_TIMEOUT).append(this.timeout);
        }
        if (this.readOnly) {
            result.append(',');
            result.append(READ_ONLY_MARKER);
        }
        return result;
    }

}
